/*
* Copyright (C) 2015 Iasc CHEN
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package me.iasc.microduino.bluejoypad;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Bundle;
import android.os.PowerManager;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.ImageButton;
import android.widget.SeekBar;
import android.widget.TextView;
import android.widget.ToggleButton;
import me.iasc.microduino.ble.BleAsyncTask;
import me.iasc.microduino.ble.BluetoothLeService;
import java.util.Timer;
import java.util.TimerTask;
/**
* For a given BLE device, this Activity provides the user interface to connect, display data,
* and display GATT services and characteristics supported by the device. The Activity
* communicates with {@code BluetoothLeService}, which in turn interacts with the
* Bluetooth LE API.
*/
public class DeviceControlActivity extends AbstractBleControlActivity
implements SensorEventListener {
private final static String TAG = DeviceControlActivity.class.getSimpleName();
private ToggleButton unlockButton;
private int FRONT_Y_2 = -64, FRONT_Y_1 = -20, BACK_Y_1 = 20, BACK_Y_2 = 64;
private int ROTATE_LEFT = 1000, ROTATE_MIDDLE = 1500, ROTATE_RIGHT = 2000;
private short CHANNEL_HIGH = 1900, CHANNEL_MID = 1500, CHANNEL_LOW = 1100;
private short CHANNEL_RANGE = (short) (CHANNEL_HIGH - CHANNEL_LOW);
private short CHANNEL_HALF_RANGE = (short) (CHANNEL_RANGE / 2);
private VerticalSeekBar powerBar;
private ImageButton rotateLeft, rotateRight;
private TextView xView, yView, zView, vView, hView, lrView, fbView;
private static Timer cmdSendTimer;
private SensorManager mSensorManager;
private Sensor mAccelerometer;
private int power = 0, rotate = 0, lrValue = 0, fbValue = 0;
private float gravity[];
private static boolean isDriving = false;
private PowerManager.WakeLock wl;
@Override
public void onCreate(Bundle savedInstanceState) {
setContentView(R.layout.activity_main);
super.onCreate(savedInstanceState);
// Initializing the gravity vector to zero.
gravity = new float[3];
gravity[0] = 0;
gravity[1] = 0;
gravity[2] = 0;
xView = (TextView) findViewById(R.id.x);
yView = (TextView) findViewById(R.id.y);
zView = (TextView) findViewById(R.id.z);
vView = (TextView) findViewById(R.id.v);
hView = (TextView) findViewById(R.id.h);
lrView = (TextView) findViewById(R.id.lr);
fbView = (TextView) findViewById(R.id.fb);
// Initializing the accelerometer stuff
// Register this as SensorEventListener
mSensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
mSensorManager.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_GAME);
powerBar = (VerticalSeekBar) findViewById(R.id.powerBar);
powerBar.setProgress(0);
powerBar.setMax(CHANNEL_RANGE);
powerBar.setEnabled(false);
rotateLeft = (ImageButton) findViewById(R.id.rotateLeft);
rotateRight = (ImageButton) findViewById(R.id.rotateRight);
rotateLeft.setEnabled(false);
rotateRight.setEnabled(false);
powerBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if (isDriving) {
if (progress < 0) progress = 0;
if (progress > CHANNEL_RANGE) progress = CHANNEL_RANGE;
power = CHANNEL_LOW + progress;
JoypadCommand.changeChannel(JoypadCommand.POWER, power);
updateUIPower(power);
}
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
// ignore
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
// ignore
}
});
rotateLeft.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (isDriving) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
rotate = ROTATE_LEFT;
} else if (event.getAction() == MotionEvent.ACTION_UP) {
rotate = ROTATE_MIDDLE;
}
JoypadCommand.changeChannel(JoypadCommand.ROTATE, rotate);
updateUIRotate(rotate);
}
return false;
}
});
rotateRight.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (isDriving) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
rotate = ROTATE_RIGHT;
} else if (event.getAction() == MotionEvent.ACTION_UP) {
rotate = ROTATE_MIDDLE;
}
JoypadCommand.changeChannel(JoypadCommand.ROTATE, rotate);
updateUIRotate(rotate);
}
return false;
}
});
unlockButton = (ToggleButton) findViewById(R.id.toggleButton);
unlockButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (unlockButton.isChecked()) {
powerBar.setEnabled(true);
rotateLeft.setEnabled(true);
rotateRight.setEnabled(true);
power = 1150;
powerBar.setProgress(power - CHANNEL_LOW);
updateUIPower(power);
UnlockTask task = new UnlockTask();
task.execute();
unlockButton.setBackgroundColor(Color.GREEN);
} else {
// Slow Down
MinusPowerTask task = new MinusPowerTask();
task.execute();
power = 1000;
powerBar.setProgress(0);
updateUIPower(power);
powerBar.setEnabled(false);
rotateLeft.setEnabled(false);
rotateRight.setEnabled(false);
unlockButton.setBackgroundColor(Color.RED);
}
}
});
// Getting a WakeLock. This insures that the phone does not sleep
// while driving the robot.
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
wl = pm.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK, "My Tag");
wl.acquire();
Intent gattServiceIntent = new Intent(this, BluetoothLeService.class);
bindService(gattServiceIntent, mServiceConnection, BIND_AUTO_CREATE);
registerReceiver(mGattUpdateReceiver, makeGattUpdateIntentFilter());
if (mBluetoothLeService != null) {
final boolean result = mBluetoothLeService.connect(currDeviceAddress);
Log.d(TAG, "Connect request result=" + result);
}
}
protected void updateReadyState(final int resourceId) {
runOnUiThread(new Runnable() {
@Override
public void run() {
wait_ble(2000);
characteristicReady = true;
isSerial.setText(getString(resourceId));
unlockButton.setEnabled(true);
toastMessage(getString(resourceId));
}
});
}
public void onAccuracyChanged(Sensor sensor, int accuracy) {
// We don't do anything when the accuracy of the accelerometer changes.
}
public void onSensorChanged(SensorEvent event) {
// This function is called repeatedly. The tempo is set when the listener is register
// see onCreate() method.
// Lowpass filter the gravity vector so that sudden movements are filtered.
float alpha = (float) 0.8;
gravity[0] = alpha * gravity[0] + (1 - alpha) * event.values[0];
gravity[1] = alpha * gravity[1] + (1 - alpha) * event.values[1];
gravity[2] = alpha * gravity[2] + (1 - alpha) * event.values[2];
// Normalize the gravity vector and rescale it so that every component fits one byte.
float size = (float) Math.sqrt(Math.pow(gravity[0], 2) + Math.pow(gravity[1], 2) + Math.pow(gravity[2], 2));
byte x = (byte) (128 * gravity[0] / size);
byte y = (byte) (128 * gravity[1] / size);
byte z = (byte) (128 * gravity[2] / size);
// Update the GUI
updateUIXyz(x, y, z);
}
void updateUIXyz(final byte x, final byte y, final byte z) {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (isDriving) {
// Calculate Left, Right or Forward, back
lrValue = calcChannelValue(-y);
JoypadCommand.changeChannel(JoypadCommand.LR, lrValue);
fbValue = calcChannelValue(x);
JoypadCommand.changeChannel(JoypadCommand.FB, fbValue);
}
xView.setText("X: " + Integer.toString(x));
yView.setText("Y: " + Integer.toString(y));
zView.setText("Z: " + Integer.toString(z));
lrView.setText("LR: " + Integer.toString(lrValue));
fbView.setText("FB: " + Integer.toString(fbValue));
}
});
}
void updateUIPower(final int v) {
runOnUiThread(new Runnable() {
@Override
public void run() {
vView.setText("PW: " + Integer.toString(v));
}
});
}
void updateUIRotate(final int v) {
runOnUiThread(new Runnable() {
@Override
public void run() {
hView.setText("RT: " + Integer.toString(v));
}
});
}
@Override
protected void onDestroy() {
super.onDestroy();
stopTimer(cmdSendTimer);
}
private Timer startSentCmdTimer(long delay, long period) {
Timer mTimer = new Timer();
mTimer.schedule(new TimerTask() {
@Override
public void run() {
byte[] cmd = JoypadCommand.compose();
// Microduino BLE firmware only can use 18 bytes buffer, so I have to split the message.
int bufferlen = BLE_MSG_BUFFER_LEN;
byte[] buffer;
for (int offset = 0; offset < cmd.length; offset += BLE_MSG_BUFFER_LEN) {
bufferlen = Math.min(BLE_MSG_BUFFER_LEN, cmd.length - offset);
buffer = new byte[bufferlen];
System.arraycopy(cmd, offset, buffer, 0, bufferlen);
// Log.v("BBuffer sub", "" + JoypadCommand.byteArrayToHexString(buffer));
sendMessage(buffer);
wait_ble(BLE_MSG_SEND_INTERVAL / 2);
buffer = null;
}
}
}, delay, period);
return mTimer;
}
private void stopTimer(Timer mTimer) {
if (mTimer != null) {
mTimer.cancel();
mTimer = null;
}
}
short calcChannelValue(int value) {
short ret = CHANNEL_MID;
if (value > BACK_Y_2) {
ret = CHANNEL_LOW;
} else if (BACK_Y_1 < value && value <= BACK_Y_2) {
ret = (short) (CHANNEL_MID - (value - BACK_Y_1) * CHANNEL_HALF_RANGE / (BACK_Y_2 - BACK_Y_1));
} else if (FRONT_Y_1 < value && value <= BACK_Y_1) {
// Stable Range, Balance
ret = CHANNEL_MID;
} else if (FRONT_Y_2 < value && value <= FRONT_Y_1) {
ret = (short) (CHANNEL_MID + (value - FRONT_Y_1) * CHANNEL_HALF_RANGE / (FRONT_Y_2 - FRONT_Y_1));
} else {
ret = CHANNEL_HIGH;
}
// Log.v(TAG, "calcChannelValue: " + ret);
return ret;
}
private class UnlockTask extends BleAsyncTask {
private final int WAIT_INTERVAL = 2000;
protected int getInterval() {
return WAIT_INTERVAL;
}
@Override
protected String doInBackground(String... params) {
// Should send unlock cmd 2 seconds
JoypadCommand.resetChannel(JoypadCommand.UNLOCK_CMD);
cmdSendTimer = startSentCmdTimer(0, BLE_MSG_SEND_INTERVAL);
wait_ble(getInterval());
return "Done";
}
@Override
protected void onPostExecute(String result) {
JoypadCommand.resetChannel(JoypadCommand.NORMAL_CMD);
isDriving = true;
toastMessage("Unlocked");
}
}
private class MinusPowerTask extends BleAsyncTask {
private final int WAIT_INTERVAL = 1000;
protected int getInterval() {
return WAIT_INTERVAL;
}
@Override
protected String doInBackground(String... params) {
isDriving = false;
JoypadCommand.resetChannel(JoypadCommand.DOWN_CMD);
// Slow down
short power = JoypadCommand.minusPower();
while (power > 1000) {
wait_ble(getInterval());
power = JoypadCommand.minusPower();
}
JoypadCommand.resetChannel(JoypadCommand.LOCK_CMD);
wait_ble(getInterval());
return "Done";
}
@Override
protected void onPostExecute(String result) {
stopTimer(cmdSendTimer);
Log.v(TAG, "sendByteArrayMsgTask onPostExecute done :" + result);
toastMessage("Stopped");
}
}
}